#include <cstring>
#include <fstream>
#include <iostream>

#include "Arguments.h"
#include "XMLRawParser.h"

// ----------------------------------------------------------------------------

namespace gpxtools
{
  class GPXPath : public XMLParserHandler
  {
  public:
    // -- Constructor -----------------------------------------------------------
    GPXPath() :
      _arguments("gpxrm [OPTION].. PATH [FILE]\nRead or set a path from a GPX-file.\n", "gpxpath v0.1", "Read or set a path from a GPX-file and display the result on standard output."),
      _value(_arguments, true,  'v', "value",       "VALUE", "set path to a value", ""),
      _xmlParser(this),
      _update(false),
      _updating(false),
      _found(false),
      _updated(false)
    {
    }

    // -- Deconstructor ---------------------------------------------------------
    virtual ~GPXPath()
    {
    }

    // -- Properties ------------------------------------------------------------

    // -- Parse arguments -------------------------------------------------------
    bool processArguments(int argc, char *argv[])
    {
      std::vector<std::string> filenames;

      if (!_arguments.parse(argc,argv, filenames)) return false;

      _updating = !_value.value().empty();

      if (!checkArguments(filenames)) return false;

      if (filenames.empty())
      {
        if (!_xmlParser.parse(std::cin)) return false;
      }
      else
      {
        if (!parseFile(filenames.front())) return false;
      }

      return _found || _updated;
    }

    // -- Check arguments ---------------------------------------------------------
    bool checkArguments(std::vector<std::string> &filenames)
    {
      if (filenames.empty())
      {
        std::cerr << "Missing path to look for." << std::endl;
        return false;
      }

      _path = filenames.front();

      if (_updating)
      {
        _parent = getParent(_path);
      }

      filenames.erase(filenames.begin());

      if (filenames.size() > 1)
      {
        std::cerr << "Too many input files."  << std::endl;
        return false;
      }

      return true;
    }

    // -- Parse a file ----------------------------------------------------------
    bool parseFile(const std::string &filename)
    {
      bool ok =false;

      std::ifstream file(filename);

      if (file.is_open())
      {
        ok = _xmlParser.parse(file);

        file.close();
      }
      else
      {
        std::cerr << "Unable to open: " << filename << std::endl;
      }

      return ok;
    }

  private:
    void store(const std::string &text)
    {
      std::cout << text;
    }

  public:
    // -- Callbacks -------------------------------------------------------------
    virtual void unhandled(const std::string &, const std::string &text)
    {
      if (_updating)
      {
        std::cout << text;
      }
    }

    virtual void startElement(const std::string &path, const std::string &text, const std::string &, const Attributes &)
    {
      if (_updating)
      {
        updateStartElement(path, text);
      }
      else
      {
        if (_path == path)
        {
          _text.clear();
          _found = true;
        }
      }
    }

    virtual void text(const std::string &path, const std::string &text)
    {
      if (_updating)
      {
        updateText(path, text);
      }
      else
      {
        if (_path == path) _text += text;
      }
    }

    virtual void endElement(const std::string &path, const std::string &text, const std::string &)
    {
      if (_updating)
      {
        updateEndElement(path, text);
      }
      else
      {
        if (_path == path)
        {
          XMLRawParser::trim(_text);

          std::cout << _text << std::endl;
        }
      }
    }

  private:
    std::string getParent(const std::string &path)
    {
      if (startsWith(path, "/gpx/metadata")) return "/gpx";

      if (startsWith(path, "/gpx/wpt")) return "/gpx/wpt";

      if (startsWith(path, "/gpx/rte/rtept")) return "/gpx/rte/rtept";

      if (startsWith(path, "/gpx/rte")) return "/gpx/rte";

      if (startsWith(path, "/gpx/trk/trkseg/trkpt")) return "/gpx/trk/trkseg/trkpt";

      if (startsWith(path, "/gpx/trk")) return "/gpx/trk";

      return std::string();
    }

    void updateStartElement(const std::string &path, const std::string &text)
    {
      if (path == _parent)
      {
        _update = true;
      }

      if (path == _path)
      {
        // Full match, reset the sub match
        _matched.clear();
      }
      else if (matchSubPath(path))
      {
        // Look for the largest sub match
        if (path.length() > _matched.length()) _matched = path;
      }
      else if (nextParent(path))
      {
        insertValue(_parent);
        _update = false;
      }

      std::cout << text;
    }

    void updateText(const std::string &path, const std::string &text)
    {
      // Skip text for matched path, it will be changed by the new value
      if (path != _path)
      {
        std::cout << text;
      }
    }

    void updateEndElement(const std::string &path, const std::string &text)
    {
      if (path == _path)
      {
        // Write new value for matched path
        std::cout << _value.value();
        _updated = true;
        _update = false;
      }
      else if (path == _matched)
      {
        // Close tag for the sub matched -> insert the tags and new value
        insertValue(path);
        _matched.clear();
        _updated = true;
        _update = false;
      }

      std::cout << text;
    }

    bool matchSubPath(const std::string &path)
    {
      if (!_update) return false;

      if (path.size() >= _path.size()) return false;

      return startsWith(_path, path);
    }

    bool nextParent(const std::string &path)
    {
      if (!_update) return false;

      if (startsWith(_path, "/gpx/metadata") &&
          ((startsWith(path, "/gpx/trk")) ||
           (startsWith(path, "/gpx/rte")) ||
           (startsWith(path, "/gpx/wpt")))) return true;

      if (!startsWith(_path, "/gpx/trk/trkseg") &&
          startsWith(path, "/gpx/trk/trkseg")) return true;

      if (!startsWith(_path, "/gpx/rte/rtept") &&
          startsWith(path, "/gpx/rte/rtept")) return true;

      return false;
    }

    bool startsWith(const std::string &str, const std::string &start)
    {
      return (str.compare(0, start.size(), start) == 0);
    }

    void insertValue(const std::string &path)
    {
      if (!matchSubPath(path)) return;

      buildTags(_path.substr(path.size() + 1)); // also remove first /
    }

    void buildTags(const std::string &tags)
    {
      std::size_t size = tags.find_first_of('/');

      if (size != std::string::npos)
      {
        const std::string name = tags.substr(0, size);

        std::cout << '<' << name << '>';

        buildTags(tags.substr(size + 1)); // also remove next /

        std::cout << "</" << name << '>';
      }
      else
      {
        std::cout << '<' << tags << '>' << _value.value() << "</" << tags << '>';
      }
    }

    // Members
    arg::Arguments _arguments;
    arg::Argument _value; // the new value

    XMLRawParser _xmlParser;

    std::string _path;    // the search path
    std::string _parent;  // the parent path of _path
    std::string _matched; // the matched sub path
    std::string _text;    // the text of the path during querying

    bool _found;
    bool _updating;       // the mode of the tool
    bool _update;         // should the path be updated in the parent
    bool _updated;        // is the path updated
  };
}

// -- Main program ------------------------------------------------------------

int main(int argc, char *argv[])
{
  gpxtools::GPXPath gpxPath;

  return gpxPath.processArguments(argc, argv) ? 0 : 1;
}
